package org.exist.client.xacml;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.net.URI;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import com.sun.xacml.Policy;
import com.sun.xacml.PolicySet;
import com.sun.xacml.PolicyTreeElement;
import com.sun.xacml.Rule;
public class TreeMutator implements ActionListener, DragGestureListener, DragSourceListener, DropTargetListener, KeyListener, MouseListener, PopupMenuListener
{
private static final String NEW_RULE = "New Rule";
private static final String NEW_POLICY = "New Policy";
private static final String NEW_POLICY_SET = "New Policy Set";
private static final String REMOVE = "Remove";
public static final int BIAS_BEFORE = -1;
public static final int BIAS_CURRENT = 0;
public static final int BIAS_AFTER = 1;
public static final int BIAS_NO_DESTINATION = -2;
private static final int BIAS_DELTA_Y = 4;
private XACMLTreeNode currentDestinationNode = null;
private int destinationBias = 0;
private NodeCopyAction copyAction;
private NodeExpander expander;
private AutoScroller scroller;
private JTree tree;
private JPopupMenu popup;
private XACMLTreeNode contextNode;
private TreeMutator() {}
public TreeMutator(JTree tree)
{
if(tree == null)
throw new NullPointerException("Tree cannot be null");
popup = new JPopupMenu();
this.tree = tree;
scroller = new AutoScroller();
expander = new NodeExpander(tree);
copyAction = new NodeCopyAction(tree);
tree.getInputMap().put(copyAction.getTrigger(), copyAction.getName());
tree.getActionMap().put(copyAction.getName(), copyAction);
tree.setDragEnabled(false);
tree.setTransferHandler(null);
tree.addMouseListener(this);
tree.addKeyListener(this);
DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(tree, DnDConstants.ACTION_COPY_OR_MOVE, this);
new DropTarget(tree, this);
reset();
}
public JTree getTree()
{
return tree;
}
public void reset()
{
contextNode = null;
copyAction.setContextNode(null);
popup.removeAll();
if(popup.isVisible())
popup.setVisible(false);
}
//MouseListener methods
public void mouseClicked(MouseEvent event)
{
showPopup(event);
}
public void mouseEntered(MouseEvent event)
{
showPopup(event);
}
public void mouseExited(MouseEvent event)
{
showPopup(event);
}
public void mousePressed(MouseEvent event)
{
showPopup(event);
}
public void mouseReleased(MouseEvent event)
{
showPopup(event);
}
private void showPopup(MouseEvent event)
{
if(!popup.isPopupTrigger(event))
return;
reset();
Object source = event.getSource();
if(source != tree)
return;
Point p = event.getPoint();
int row = tree.getClosestRowForLocation(p.x, p.y);
if(row == -1)
{
showRootPopup(p);
return;
}
Rectangle bounds = tree.getRowBounds(row);
if(bounds.y > p.y || bounds.y + bounds.height <= p.y)
{
showRootPopup(p);
return;
}
TreePath path = tree.getPathForRow(row);
if(path == null)
{
showRootPopup(p);
return;
}
Object last = path.getLastPathComponent();
XACMLTreeNode node = (XACMLTreeNode)last;
copyAction.setContextNode(node);
if(!(last instanceof PolicyElementNode))
{
popup.add(copyAction);
popup.show(tree, p.x, p.y);
return;
}
contextNode = (XACMLTreeNode)last;
handleTreeElementNode();
popup.addSeparator();
popup.add(copyAction);
popup.show(tree, p.x, p.y);
}
private void handleTreeElementNode()
{
if(contextNode instanceof PolicySetNode)
{
addPolicySetItem();
addPolicyItem();
}
else if(contextNode instanceof PolicyNode)
addRuleItem();
//else if(contextNode instanceof Rule)
// do nothing in this case
addRemoveItem();
}
private void addRemoveItem()
{
JMenuItem remove = new JMenuItem(REMOVE, KeyEvent.VK_R);
remove.addActionListener(this);
popup.add(remove);
}
private void addRuleItem()
{
JMenuItem newRule = new JMenuItem(NEW_RULE, KeyEvent.VK_R);
newRule.addActionListener(this);
popup.add(newRule);
}
private void addPolicyItem()
{
JMenuItem newPolicy = new JMenuItem(NEW_POLICY, KeyEvent.VK_P);
newPolicy.addActionListener(this);
popup.add(newPolicy);
}
private void addPolicySetItem()
{
JMenuItem newPolicySet = new JMenuItem(NEW_POLICY_SET, KeyEvent.VK_S);
newPolicySet.addActionListener(this);
popup.add(newPolicySet);
}
private void showRootPopup(Point p)
{
contextNode = getRootNode();
addPolicySetItem();
addPolicyItem();
popup.show(tree, p.x, p.y);
}
private RootNode getRootNode()
{
TreeModel model = tree.getModel();
if(!(model instanceof XACMLTreeModel))
return null;
XACMLTreeModel xmodel = (XACMLTreeModel)model;
return (RootNode)xmodel.getRoot();
}
private void newRule()
{
if(contextNode instanceof PolicyNode)
{
PolicyNode node = (PolicyNode)contextNode;
Rule rule = XACMLEditor.createDefaultRule(node);
node.add(rule);
}
}
private void newPolicySet()
{
if(contextNode instanceof PolicySetNode || contextNode instanceof RootNode)
{
PolicyElementContainer node =((PolicyElementContainer)contextNode);
PolicySet ps = XACMLEditor.createDefaultPolicySet(node);
node.add(ps);
}
}
private void newPolicy()
{
if(contextNode instanceof PolicySetNode || contextNode instanceof RootNode)
{
PolicyElementContainer node =((PolicyElementContainer)contextNode);
Policy p = XACMLEditor.createDefaultPolicy(node);
node.add(p);
}
}
private void remove()
{
if(contextNode == null)
return;
NodeContainer parent = contextNode.getParent();
if(parent instanceof PolicyElementContainer && contextNode instanceof PolicyElementNode)
((PolicyElementContainer)parent).remove((PolicyElementNode)contextNode);
}
public void actionPerformed(ActionEvent event)
{
String actionCommand = event.getActionCommand();
if(actionCommand == null)
return;
else if(actionCommand.equals(NEW_RULE))
newRule();
else if(actionCommand.equals(NEW_POLICY))
newPolicy();
else if(actionCommand.equals(NEW_POLICY_SET))
newPolicySet();
else if(actionCommand.equals(REMOVE))
remove();
tree.revalidate();
tree.repaint();
}
//KeyListener methods
public void keyPressed(KeyEvent event)
{
//avoid collisions with JTree's builtin bindings
if(event.isShiftDown() || event.isControlDown() || !event.isAltDown())
return;
int keyCode = event.getKeyCode();
int delta;
if(keyCode == KeyEvent.VK_UP)
delta = -1;
else if(keyCode == KeyEvent.VK_DOWN)
delta = 1;
else
return;
TreePath selected = tree.getSelectionPath();
if(selected == null)
return;
XACMLTreeNode treeNode = (XACMLTreeNode)selected.getLastPathComponent();
if(!(treeNode instanceof PolicyElementNode))
return;
PolicyElementNode node = (PolicyElementNode)treeNode;
PolicyElementContainer parent = (PolicyElementContainer)node.getParent();
int currentIndex = parent.indexOfChild(node);
if(currentIndex < 0)
return;
currentIndex += delta;
if(currentIndex < 0 || currentIndex >= parent.getChildCount())
return;
if(currentIndex == 0 && !(parent instanceof RootNode))
return;
tree.clearSelection();
parent.remove(node);
parent.add(currentIndex, node);
tree.setSelectionPath(XACMLTreeModel.getPathToNode(node));
}
public void keyReleased(KeyEvent event) {}
public void keyTyped(KeyEvent event) {}
//PopupMenuListener methods
public void popupMenuCanceled(PopupMenuEvent arg0) {}
public void popupMenuWillBecomeInvisible(PopupMenuEvent event)
{
reset();
}
public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) {}
// DragGestureListener method
public void dragGestureRecognized(DragGestureEvent event)
{
Point location = event.getDragOrigin();
TreePath path = tree.getPathForLocation(location.x, location.y);
if(path == null)
return;
int action = event.getDragAction();
XACMLTreeNode transferNode = (XACMLTreeNode)path.getLastPathComponent();
Cursor cursor = (action == DnDConstants.ACTION_MOVE) ? DragSource.DefaultMoveDrop : DragSource.DefaultCopyDrop;
event.startDrag(cursor, new NodeTransferable(transferNode), this);
}
// DropTargetListener methods
public void drop(DropTargetDropEvent event)
{
event.acceptDrop(event.getDropAction());
boolean success = false;
try
{
success = handleDrop(event);
}
//these exceptions should not happen:
// the flavor is checked and the returned
// data requires no IO
catch(IOException ioe)
{
success = false;
}
catch(UnsupportedFlavorException ufe)
{
success = false;
}
finally
{
haltTimers();
clearDestination();
event.dropComplete(success);
}
}
public void dragOver(DropTargetDragEvent event)
{
checkDrag(event);
}
public void dragEnter(DropTargetDragEvent event)
{
checkDrag(event);
}
public void dropActionChanged(DropTargetDragEvent event)
{
checkDrag(event);
}
public void dragExit(DropTargetEvent event)
{
haltTimers();
clearDestination();
}
private void haltTimers()
{
scroller.stop();
expander.stop();
}
private void checkDrag(DropTargetDragEvent event)
{
XACMLTreeNode oldNode = currentDestinationNode;
int oldBias = destinationBias;
Point location = event.getLocation();
updateCurrentDestination(location, event.getDropAction());
if(currentDestinationNode == null)
{
expander.stop();
clearDestination();
event.rejectDrag();
return;
}
scroller.autoscroll(tree, location);
if(destinationBias != BIAS_CURRENT)
expander.stop();
else if(oldNode != currentDestinationNode || destinationBias != oldBias)
expander.hover(currentDestinationNode);
if(supportsDrop(event))
repaintDestination(oldNode, oldBias);
else
clearDestination();
}
private boolean supportsDrop(DropTargetDragEvent event)
{
int action = event.getDropAction();
boolean supported;
updateCurrentDestination(event.getLocation(), action);
if(currentDestinationNode == null)
supported = false;
else if(event.isDataFlavorSupported(NodeTransferable.TARGET_FLAVOR))
{
if(action == DnDConstants.ACTION_COPY_OR_MOVE || action == DnDConstants.ACTION_MOVE)
action = DnDConstants.ACTION_COPY;
supported = isTargetDropValid(action);
}
else if(event.isDataFlavorSupported(NodeTransferable.CONDITION_FLAVOR))
{
if(action == DnDConstants.ACTION_COPY_OR_MOVE || action == DnDConstants.ACTION_MOVE)
action = DnDConstants.ACTION_COPY;
supported = isConditionDropValid(action);
}
else if(event.isDataFlavorSupported(NodeTransferable.RULE_FLAVOR))
supported = isRuleDropValid(action);
else if(event.isDataFlavorSupported(NodeTransferable.ABSTRACT_POLICY_FLAVOR))
supported = isAbstractPolicyDropValid(action);
else
supported = false;
if(supported)
event.acceptDrag(action);
else
event.rejectDrag();
return supported;
}
private boolean isTargetDropValid(int action)
{
if(action == DnDConstants.ACTION_MOVE)
return false;
if(currentDestinationNode instanceof PolicyElementNode || currentDestinationNode instanceof TargetNode)
return destinationBias == TreeMutator.BIAS_CURRENT;
return false;
}
private boolean isConditionDropValid(int action)
{
if(action == DnDConstants.ACTION_MOVE)
return false;
if(currentDestinationNode instanceof ConditionNode)
return destinationBias == TreeMutator.BIAS_CURRENT;
if(currentDestinationNode instanceof RuleNode || currentDestinationNode instanceof ConditionNode)
return destinationBias == TreeMutator.BIAS_CURRENT;
return false;
}
private boolean isRuleDropValid(int action)
{
if(currentDestinationNode instanceof PolicyNode)
return destinationBias == TreeMutator.BIAS_CURRENT;
if(currentDestinationNode instanceof RuleNode)
return destinationBias == TreeMutator.BIAS_AFTER || destinationBias == TreeMutator.BIAS_BEFORE;
if(currentDestinationNode instanceof TargetNode && currentDestinationNode.getParent() instanceof PolicyNode)
return destinationBias == TreeMutator.BIAS_AFTER;
return false;
}
private boolean isAbstractPolicyDropValid(int action)
{
if(currentDestinationNode instanceof PolicySetNode || currentDestinationNode instanceof RootNode)
return true;
if(currentDestinationNode instanceof PolicyNode)
return destinationBias == TreeMutator.BIAS_AFTER || destinationBias == TreeMutator.BIAS_BEFORE;
if(currentDestinationNode instanceof TargetNode && currentDestinationNode.getParent() instanceof PolicySetNode)
return destinationBias == TreeMutator.BIAS_AFTER;
return false;
}
private boolean isPolicyElementDropValid(int action, PolicyElementNode srcNode)
{
if(srcNode instanceof RuleNode)
return isRuleDropValid(action);
else if(srcNode instanceof AbstractPolicyNode)
return isAbstractPolicyDropValid(action);
else
return false;
}
private boolean handleDrop(DropTargetDropEvent event) throws IOException, UnsupportedFlavorException
{
Transferable data = event.getTransferable();
int action = event.getDropAction();
updateCurrentDestination(event.getLocation(), event.getDropAction());
if(currentDestinationNode == null)
return false;
if(data.isDataFlavorSupported(NodeTransferable.TARGET_FLAVOR))
{
if(!isTargetDropValid(action))
return false;
TargetNode destTarget;
if(currentDestinationNode instanceof PolicyElementNode)
destTarget = ((PolicyElementNode)currentDestinationNode).getTarget();
else if(currentDestinationNode instanceof TargetNode)
destTarget = (TargetNode)currentDestinationNode;
else
return false;
TargetNode source = (TargetNode)data.getTransferData(NodeTransferable.TARGET_FLAVOR);
destTarget.setTarget(source.getTarget());
return true;
}
if(data.isDataFlavorSupported(NodeTransferable.CONDITION_FLAVOR))
{
if(!isConditionDropValid(action))
return false;
ConditionNode destCondition;
if(currentDestinationNode instanceof RuleNode)
destCondition = ((RuleNode)currentDestinationNode).getCondition();
else if(currentDestinationNode instanceof ConditionNode)
destCondition = (ConditionNode)currentDestinationNode;
else
return false;
ConditionNode source = (ConditionNode)data.getTransferData(NodeTransferable.CONDITION_FLAVOR);
destCondition.setCondition(source.getCondition());
return true;
}
if(data.isDataFlavorSupported(NodeTransferable.POLICY_ELEMENT_FLAVOR))
{
PolicyElementNode srcNode = (PolicyElementNode)data.getTransferData(NodeTransferable.POLICY_ELEMENT_FLAVOR);
PolicyElementContainer oldParent = (PolicyElementContainer)srcNode.getParent();
if(!isPolicyElementDropValid(action, srcNode))
return false;
PolicyElementContainer newParent;
if(destinationBias == TreeMutator.BIAS_CURRENT)
newParent = (PolicyElementContainer)currentDestinationNode;
else
newParent = (PolicyElementContainer)currentDestinationNode.getParent();
if(isDescendantOrSelf(srcNode, newParent))
return false;
if(action == DnDConstants.ACTION_MOVE)
{
if(oldParent != null)
oldParent.remove(srcNode);
}
int insertionIndex = newParent.indexOfChild(currentDestinationNode);
if(insertionIndex < 0)
insertionIndex = newParent.getChildCount();
else if(destinationBias == TreeMutator.BIAS_AFTER)
insertionIndex++;
if(action == DnDConstants.ACTION_MOVE && oldParent == newParent)
newParent.add(insertionIndex, srcNode);
else
{
PolicyTreeElement copy;
String currentId = srcNode.getId().toString();
if(newParent.containsId(currentId))
copy = srcNode.create(URI.create(XACMLEditor.createUniqueId(newParent, currentId)));
else
copy = srcNode.create();
newParent.add(insertionIndex, copy);
}
return true;
}
return false;
}
private boolean isDescendantOrSelf(PolicyElementNode srcNode, PolicyElementContainer newParent)
{
TreePath srcPath = XACMLTreeModel.getPathToNode(srcNode);
TreePath newParentPath = XACMLTreeModel.getPathToNode(newParent);
if(srcPath == null || newParentPath == null)
return false;
return srcNode == newParent || srcPath.isDescendant(newParentPath);
}
private void updateCurrentDestination(Point location, int dropAction)
{
TreePath currentPath = tree.getClosestPathForLocation(location.x, location.y);
if(currentPath == null)
{
currentDestinationNode = null;
destinationBias = BIAS_NO_DESTINATION;
return;
}
currentDestinationNode = (XACMLTreeNode)currentPath.getLastPathComponent();
int row = tree.getRowForPath(currentPath);
Rectangle bounds = tree.getRowBounds(row);
if(bounds.y > location.y || location.y < BIAS_DELTA_Y)
destinationBias = BIAS_BEFORE;
else if(bounds.y + bounds.height <= location.y)
destinationBias = BIAS_AFTER;
else
{
if(isDestinationDifferent(tree.getClosestPathForLocation(location.x, location.y - BIAS_DELTA_Y)))
destinationBias = BIAS_BEFORE;
else if(isDestinationDifferent(tree.getClosestPathForLocation(location.x, location.y + BIAS_DELTA_Y)))
destinationBias = BIAS_AFTER;
else
destinationBias = BIAS_CURRENT;
}
}
private boolean isDestinationDifferent(TreePath path)
{
return (path == null) ? (currentDestinationNode != null) : (currentDestinationNode != path.getLastPathComponent());
}
public int getDestinationBias(XACMLTreeNode testNode)
{
return (currentDestinationNode == null || destinationBias == BIAS_NO_DESTINATION || currentDestinationNode != testNode) ? BIAS_NO_DESTINATION : destinationBias;
}
private void clearDestination()
{
XACMLTreeNode oldNode = currentDestinationNode;
int oldBias = destinationBias;
currentDestinationNode = null;
destinationBias = BIAS_NO_DESTINATION;
repaintDestination(oldNode, oldBias);
}
private void repaintDestination(XACMLTreeNode oldNode, int oldBias)
{
if(oldNode != null && oldBias != BIAS_NO_DESTINATION)
handleRepaintDestination(oldNode, oldBias);
if(currentDestinationNode != null && destinationBias != BIAS_NO_DESTINATION)
handleRepaintDestination(currentDestinationNode, destinationBias);
}
private void handleRepaintDestination(XACMLTreeNode node, int bias)
{
if(node == null || bias == BIAS_NO_DESTINATION)
return;
int row = tree.getRowForPath(XACMLTreeModel.getPathToNode(node));
repaintRow(row);
if(bias == BIAS_AFTER)
{
if(row+1 < tree.getRowCount())
repaintRow(row+1);
}
else if(bias == BIAS_BEFORE)
{
if(row > 0)
repaintRow(row - 1);
}
}
private void repaintRow(int row)
{
Rectangle rect = tree.getRowBounds(row);
if (rect != null)
tree.repaint(rect);
}
//DragSourceListener methods
public void dropActionChanged(DragSourceDragEvent event) { }
public void dragEnter(DragSourceDragEvent event) {}
public void dragOver(DragSourceDragEvent event) {}
public void dragDropEnd(DragSourceDropEvent event) {}
public void dragExit(DragSourceEvent event) {}
}